package edu.cmu.arch.pipesandfilters;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import edu.cmu.arch.pipesandfilters.frame.Frame;
import edu.cmu.arch.pipesandfilters.utils.FrameUtils;

/**
 * This is an abstract extension to the basic FilterFramework.  You must 
 * implement a specific frame based on this (extend this) in order to take
 * advantage of the readFrame and writeFrame methods.
 * 
 * @author Kyle Benedict (jbenedic@andrew.cmu.edu)
 */
public abstract class FrameFilterFramework extends FilterFramework {
	/**
	 * byteArrayOutputStream is used to buffer in bytes from the
	 * default pipe framework, which only allows for one byte at a time.
	 * In order to generate a frame, n number (it varies, making this 
	 * necessary) of bytes will make a frame.  Any bytes read that exceed
	 * the frame must be held onto and used as the start of the next frame.
	 */
	private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
	
	/**
	 * This method reads enough bytes from the pipe in order to generate a
	 * Frame.  
	 * @return Frame
	 * @throws EndOfStreamException
	 */
	public Frame readFrame() throws EndOfStreamException, IOException {
		Frame frame = null;
		byte databyte = 0;	
		int byteArrPos = 0;
		int bytesRead = 0;
		while (byteArrPos < FrameUtils.BUFFER_SIZE) { //a buffer that should be slightly
													  //larger than a Frame.  The goal is
													  //to send in sufficient number of
													  //bytes to build the full Frame, otherwise
													  //the Frame will not create.
			try {				
				databyte = ReadFilterInputPort();
				bytesRead++;
				byteArrayOutputStream.write(databyte);
				byteArrPos++;
			} catch (EndOfStreamException e) {			
				break;
			}
		}
		ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
		byteArrPos = 0;
		try {
			frame = FrameUtils.frameFromStream(byteArrayInputStream);				
			//Recover unused portion of byte stream and reuse it in the
			//next iteration.
			int bytesAvailable = byteArrayInputStream.available();
			if (bytesAvailable != 0) {
				// there is data left on the input stream even
				// after we pulled off a frame, add it 
				// back to the output stream
				byte[] byteArr = new byte[bytesAvailable];
				byteArrayOutputStream.reset();
				byteArrayInputStream.read(byteArr);
				byteArrayOutputStream.write(byteArr);
				byteArrPos = bytesAvailable;
			} else {
				// nothing read, end of stream
				throw new EndOfStreamException();
			}
		} catch (IOException e) {
			System.out.println("IOException extracting a Frame from the stream: " + e.getMessage());
			e.printStackTrace();
			throw e;
		}
		return frame;
	}

	/**
	 * This method writes a Frame to the pipe output stream.
	 * @param frame
	 */
	public void writeFrame(Frame frame) throws IOException {
		try {
			ByteArrayOutputStream oStream = FrameUtils.frameToStream(frame);
			if (oStream != null) {
				// Far from efficient, copies the entire stream to a byte
				// array in order to write it out one byte at a time.
				byte[] byteArr = oStream.toByteArray();
				for (byte b: byteArr) {
					WriteFilterOutputPort(b);
				}
			}
		} catch (IOException ioe) {
			System.out.println("IOException converting a Frame to bytes: " + ioe.getMessage());
			ioe.printStackTrace();
			throw ioe;			
		}
	}
	
} 